今天要介紹的是 Render Props 模式,這也屬於 React 的模式之一。
Render Props 的目的類似 Higner-Order Component(HOC),兩者目的都是為了能在多個元件間共享、重用邏輯。
Render Prop 指的是一個元件的 prop,這個 prop 接收一個會傳回 JSX element 的函式,程式碼範例如下:
// render 這個 prop 接收 () => <h1>Hello</h1> 這個函式值,這個函式會回傳一個 JSX element
<MyComponent render={() => <h1>Hello</h1>} />
接收此 prop 的 MyComponent
元件本身就不需實作自己的渲染邏輯,而是呼叫 render
這個 prop 來渲染:
const MyComponent = ({ render }) => render(); // MyComponent 呼叫外部傳給他的 render prop,這個 render 函式會決定 MyComponent 要渲染的 element
雖然此模式稱為 Render Props,但 prop 名稱不一定要叫 render,任何能回傳 JSX element 的 prop 都可視為 render prop,且一個元件也可以接收多個 render prop,例如以下範例,Title
元件接收了 3 個 render prop。
const Title = (props) => (
<>
{props.renderFirstComponent()}
{props.renderSecondComponent()}
{props.renderThirdComponent()}
</>
);
export default function App() {
return (
<div>
<Title
renderFirstComponent={() => <h1>✨ First render prop! ✨</h1>}
renderSecondComponent={() => <h2>🔥 Second render prop! 🔥</h2>}
renderThirdComponent={() => <h3>🚀 Third render prop! 🚀</h3>}
/>
</div>
);
}
接收 render prop 的元件通常不只是單純呼叫 render prop,而會傳遞參數給 render prop,讓 render 函式接收資料後渲染。
function MyComponent({ render }) {
const data = { userName: "FooBar", age: 18 }; // Component 內部有需要渲染的 data
return render(data); // MyComponent 將 data 作為參數傳給 render prop 來渲染,這樣 MyComponent 就不需自己實作渲染邏輯,只要傳遞資料
}
export default function App() {
return (
// 外部呼叫 MyComponent 時,由 render prop 來決定如何渲染 data 資料
<MyComponent
render={(data) => <h1>{`this is my data, ${JSON.stringify(data)}`}</h1>}
/>
);
}
在 React 應用中,如果要讓兄弟元件能共享狀態、存取同一個值時,就需要提升狀態到共同父元件。例如以下範例,有 3 個元件共用狀態,一個是可讓使用者輸入值的 Input
元件,一個是顯示目前輸入的字數統計的 CharacterCount
元件和一個預覽輸入值的 Preview
元件,他們都需要取得使用者輸入的值,因此需將狀態提升到父元件。
import { useState } from "react";
function Input({ value, handleChange }) {
return (
<input
type="text"
value={value}
onChange={(e) => handleChange(e.target.value)}
/>
);
}
function CharacterCount({ value }) {
return <p>字數統計: {value.length}</p>;
}
function Preview({ value }) {
return <p>預覽: {value}</p>;
}
export default function App() {
const [value, setValue] = useState("");
return (
<>
<h1>Shared Value Example</h1>
<Input value={value} handleChange={setValue} />
<CharacterCount value={value} />
<Preview value={value} />
</>
);
}
然而,提升狀態後,可能會讓不需 re-render 的子元件被重新渲染,進而影響效能。而使用 render props 模式可解決提升狀態的問題,解決方式就是讓 Input
元件接收 render prop,在 Input
元件內呼叫 render()
來渲染 CharacterCount
和 Preview
區塊。修改後的程式碼如下,我們讓 Input
自己管理輸入值的 state,並接收 render prop,傳參數給 render prop 來讓CharacterCount
和 Preview
元件能根據值渲染,因此避免了提升狀態到共同父元件。
import { useState } from "react";
function Input({ render }) {
const [value, setValue] = useState("");
return (
<div>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
{/* 呼叫 render() 並傳遞 value */}
{render(value)}
</div>
);
}
function CharacterCount({ value }) {
return <p>字數統計: {value.length}</p>;
}
function Preview({ value }) {
return <p>預覽: {value}</p>;
}
export default function App() {
return (
<>
<h1>Shared Value Example with Encapsulated State</h1>
<Input
render={(value) => (
<>
<CharacterCount value={value} />
<Preview value={value} />
</>
)}
/>
</>
);
}
除了將要渲染的子元件傳給 render prop,也可以將值傳給 children
prop 來渲染,用意相同,技術上來講這也算 render prop,這樣就不用擔心 render prop 的名稱,直接以現有的 children
prop 來渲染即可,延續上面範例,可改成這樣:
import { useState } from "react";
function Input({ children }) {
const [value, setValue] = useState("");
return (
<div>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
{/* 呼叫 children prop 傳入的值來渲染 */}
{children(value)}
</div>
);
}
//...
export default function App() {
return (
<>
<h1>Shared Value Example with Encapsulated State</h1>
<Input>
{(value) => (
<>
<CharacterCount value={value} />
<Preview value={value} />
</>
)}
</Input>
</>
);
}